#!/bin/bash
# $Id: bopt_init.sh 5 2008-10-28 18:06:25Z shawnchin $
# ===========================================================================
# Copyright (c) 2008, Lee-Shawn Chin
# All rights reserved.

# Redistribution and use in source and binary forms, with or without 
# modification, are permitted provided that the following conditions 
# are met:
#
#  * Redistributions of source code must retain the above copyright 
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright 
#    notice, this list of conditions and the following disclaimer in 
#    the documentation and/or other materials provided with the distribution.
#  * The names of the its contributors may be used to endorse or promote 
#    products derived from this software without specific prior written 
#    permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===========================================================================

# TODOS:
# ------
# * handle '-' option (used for representing stderr/stdout)
# * handle "--" to end option list
# * handle "--longopt=arg" (when done, update print_usage_string__)
# * handle merging of short options (-xyz for -x -y -z)
# * Allow users to provide name for expected arguments (for use in
#   usage string.
# * generate --help page
# * create unit tests
# * detect duplicate long/short options
# * autocomplete long options (--ve,--versi >> --version)
# * when printing long description, don't split sentences in the middle of
#   words. Do something more intelligent.
# * Allow users to add more data to --version data (auth name, copyright
#   info, etc). bopt_var_appversion_more?
#
# * Create unit tests
# * DOCUMENT EVERYTHING!!!!!!!!!!
#
#
# Internal vars -------------------------------------------------------------

# Default strings
# __xyz__ will be used if bopt_var_xyz is not
__usage__="( usage text )"
__appname__=`basename $0`
__appversion__="<app_version>"
__bugreport__="<bugreport_email_address>"

# User constants
readonly BOPT_ONE_OR_MORE=99999999
readonly BOPT_ZERO_OR_MORE=99999998

# labels
readonly _TXT_EPREFIX="(bopt) ERROR:"

# Return codes
readonly _SUCCESS=0
readonly _ERR_USAGE=1
readonly _ERR_FUSAGE=101

# keyword pattern matching
readonly _kw_varname="[:alnum:]-_"   # variable name
readonly _kw_alpha="[:alpha:]"       # alphabets
readonly _kw_alnum="[:alnum:]"       # alpha-numeric
readonly _kw_numeric="[:digit:]"     # numeric


# Arg mgmt
declare -a _arg_list
_arg_count=0 # by default, assume no args wanted
_arg_found=0 # how many parsed so far

# Option mgmt
declare -a _opt_long
declare -a _opt_short
declare -a _opt_args
declare -a _opt_desc
declare -a _opt_selected
declare -a _opt_argval
_opt_soptlist=""
_opt_size=0

# flags
_bopt_parsed=false
_bopt_defopts=true
_bopt_defopts_applied=false

# User functions ------------------------------------------------------------

# Disable default options such as --help and --version.
function bopt_disable_default_options {
    _bopt_defopts=false
}

# Add an option
# Usage: bopt_add_option "long_option" "short_option" "argument" "descr" 
# Eg: bopt_add_options "verbose" "v" "ARG_NAME" "Print full details"
function bopt_add_option {
    
    # TODO: Check if lopt or sopt already exist
 
    fname="bopt_add_option"
    # Globar vars used:
    ##  _opt_long
    ##  _opt_short
    ##  _opt_args
    ##  _opt_desc
    ##  _opt_size
    
    # grab input args
    local lopt=$1
    local sopt=$2
    local args=$3
    local desc=$4
    
    # acceptable chars for long options"
    
    # --- sanity checks ---
    
    # We expect 4 args
    if [ $# -ne 4 ]; then
        perror_fusage__ $BASH_LINENO $fname "expects 4 args ($# given)" $@
    fi

    # First arg must be a alphanumeric chars, - or _
    if ! `is_valid__  "$lopt" "$_kw_varname"`; then
        perror_fusage__ $BASH_LINENO $fname "1st arg has invalid chars" $@
    fi

    # Second arg must be a single char
    if [ ${#sopt} -ne 1 ] || ! `is_valid__  "$sopt" "$_kw_alpha"`; then
        perror_fusage__ $BASH_LINENO $fname "2nd arg must be a single alphabet" $@
    fi

    # Third arg must be either a alphanum char string, or a 0 (for no arg)
    if [ "$args" != "0" ] && ! `is_valid__  "$args" "$_kw_varname"`; then
        perror_fusage__ $BASH_LINENO $fname "3rg arg must be either 0 or a valid keyword" $@
    fi

    _opt_soptlist="${_opt_soptlist}${sopt}"
    _opt_short[$_opt_size]=$sopt
    _opt_long[$_opt_size]=$lopt
    _opt_args[$_opt_size]=$args
    _opt_desc[$_opt_size]=$desc
    _opt_argval[$_opt_size]=""
    _opt_selected[$_opt_size]=false

    let _opt_size+=1

    return $_SUCCESS
}

# start_parsing_arguments
function bopt_parse {
    
    # internal temp var
    local index=0
    local lopt=""
    local sopt=""
    local arg=""
    local argval=""
    
    if ! $_bopt_parsed; then
        perror_fusage__ $BASH_LINENO "bopt_parse" "bopt_apply.sh not yet sourced" $@
    fi

    # update internal vars if specified by users
    __usage__=${bopt_var_usage:=$__usage__}
    __appname__=${bopt_var_appname:=$__appname__}
    __bugreport__=${bopt_var_bugreport:=$__bugreport__}
    __appversion__=${bopt_var_appversion:=$__appversion__}

    # loop through input args
    until [ -z $1 ]; do
        
        arg=$1
        # if arg does not start with "-", treat as argument
        if [ "${arg:0:1}" != "-" ]; then
            _arg_list[$_arg_found]=$1 # add to arg list
            let _arg_found+=1         # increment counter

            if [ $_arg_found -gt $_arg_count ]; then
                perror_usage__ "Too many arguments ($_arg_count expected)"
            fi

            shift # remove arg from list
            continue
        
        # detect long options
        elif [ "${arg:0:2}" == "--" ]; then
            lopt=${arg:2} # remove "--"

            # TODO: Handle --option=value

            # check if lopt valid
            if ! `is_lopt__ "$lopt"`; then
                perror_usage__ "Unknown option --$lopt"
            fi

            # register option selection
            eval "export o_$lopt=true"
            
            # map lopt to index
            eval "index=\$_map_ltoi_$lopt"

            # check if args needed for this option
            if [ ${_opt_args[$index]} != 0 ]; then
                argval="$2"

                # argval shouldn't be empty or start with "-"
                if [ -z $argval ] || [ "${argval:1:1}" == "-" ]; then
                    perror_usage__ "Option --$lopt requires an argument (${_opt_args[$index]})"
                fi

                # register option argument
                eval "export v_$lopt=\"$argval\""
                
                shift 2 # remove lopt and argval from list
                continue
            fi

            shift
            continue
            

        # short options "-?"
        elif [ ${#arg} -eq 2 ]; then
            # grab first char after '-'
            sopt=${arg:1:1}

            # check if sopt valid
            if ! `is_sopt__ "$sopt"`; then
                perror_usage__ "Unknown option -$sopt"
            fi

            # get equiv long option
            eval "lopt=\"\$_map_stol_$sopt\""
            
            # register option selection
            eval "export o_$lopt=true"
            
            # map lopt to index
            eval "index=\$_map_ltoi_$lopt"

            # check if args needed for this option
            if [ ${_opt_args[$index]} != 0 ]; then

                argval="$2"

                # argval shouldn't be empty or start with "-"
                if [ -z $argval ] || [ "${argval:1:1}" == "-" ]; then
                    perror_usage__ "Option --$lopt requires an argument (${_opt_args[$index]})"
                fi

                # register option argument
                eval "export v_$lopt=\"$argval\""
                
                shift 2 # remove lopt and argval from list
                continue
            fi
            # if there are args, shift 2
            shift 
            continue

        # catch everything else we may have missed
        else
            perror_usage__ "Unkown option $arg"            
        fi
        
    done

    if $_bopt_defopts; then
        if $o_help; then bopt_print_help; exit 0; fi
        if $o_version; then bopt_print_version; exit 0; fi
    fi

    # make sure we found all required arguments
    if [ $_arg_count == $BOPT_ZERO_OR_MORE ]; then
        true # nothing to do
    elif [ $_arg_count == $BOPT_ONE_OR_MORE ]; then
        if [ $_arg_found -eq 0 ]; then
            perror_usage__ "At least one argument expected"
        fi
    else # fixed number
        if [ $_arg_found -ne $_arg_count ]; then
            perror_usage__ "$_arg_count arguments expected ($_arg_found given)"
        fi
    fi

}

# specify number of args expected
function bopt_expected_args {
    
    count=$1
    
    # this func expects one integer arg
    if [ $# -ne 1 ] || ! `is_valid__ "$count" "$_kw_numeric"`; then
        perror_fusage__ $BASH_LINENO "bopt_expected_arg" "One integer argument expected" $@
    fi

    # set global var
    _arg_count=$count
}

# get specific input argument
function bopt_get_arg {

    index=$1
    
    # this func expects one integer arg
    if [ $# -ne 1 ] || ! `is_valid__ "$index" "$_kw_numeric"`; then
        perror_fusage__ $BASH_LINENO "bopt_get_arg" "One integer argument expected" $@
    fi

    echo ${_arg_list[$index]}
}

# get list of input arguments
function bopt_get_arglist {
    echo ${_arg_list[@]}
}

# get number of input arguments
function bopt_get_argcount {
    echo ${#_arg_list[@]}
    return ${#_arg_list[@]}
}

function bopt_print_help {

    print_usage_string__
    echo -e "\n$__usage__\n\n"
    echo -e "More options:\n"
    print_options_table__
    cat << EOF

Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.

Report bugs to $__bugreport__.

EOF
}

function bopt_print_version {
    echo "$__appname__ ($__appversion__)"
}

# Internal functions --------------------------------------------------------

# print error message for end user
function perror_usage__ {
    echo "$__appname__: $@" >&2
    echo "Try '$__appname__ --help' for more information" >&2
    exit $_ERR_USAGE
}

# print error message for bopt users (script writer)
function perror_fusage__ {
    ln=$1
    fname=$2
    msg=$3
    shift 3 # discard first 2 params
    echo "$_TXT_EPREFIX $fname: $msg" >&2
    echo "$ln:$fname $@" >&2
    exit $_ERR_FUSAGE
}

# check if keyword (may be used as varname) is valid
function is_valid__ {

    local kword="$1"
    local valid_chars="$2"

    # check for invalid chars
    local unmatched=`echo -n "$kword" | tr -d "${valid_chars}"`
    if [ "$unmatched" != "" ]; then 
        echo -n false;
    else
        echo -n true;
    fi
}

# check if is valid short option
function is_sopt__ {
    
    option=$1
    list=$_opt_soptlist

    # RHS=(LHS with $option removed)
    # hence, if RHS != LHS, $option exist in list
    if [ "$list" != "${list/$option/}" ]
    then
        echo -n true
        return 0
    else
        echo -n false
        return 1
    fi
}

# check if is valid long option
function is_lopt__ {
    
    lopt=$1
    found=true
    rc=0
    
    eval "if [ -z \$o_$lopt ]; then found=false; rc=1; fi"

    echo -n $found
    return $rc
}

# prints usage string
function print_usage_string__ {

    local readonly TABSIZE=10
    local readonly LW=78 # Line width
    local i=0
    local used=0
    local nextprint=0
    local sopt_compact_list=""
    local sopt_list=""
    local lopt_list=""
    local args_list=""
    local text=""
    

    # build strings for options
    while [ $i -lt $_opt_size ]; do
        if [ "${_opt_args[$i]}" == "0" ]; then # no args
            sopt_compact_list="$sopt_compact_list${_opt_short[$i]}"
            lopt_list="$lopt_list [--${_opt_long[$i]}]"
        else
            sopt_list="$sopt_list [-${_opt_short[$i]}|${_opt_args[$i]}]"
            lopt_list="$lopt_list [--${_opt_long[$i]}|${_opt_args[$i]}]"
        fi

        let i+=1
    done
    
    # build string for arguments
    if   [ $_arg_count -eq $BOPT_ZERO_OR_MORE ]; then
        args_list="(ARG...)"
    elif [ $_arg_count -eq $BOPT_ONE_OR_MORE ]; then
        args_list="ARG_1 (ARG_N...)"
    elif [ $_arg_count -eq 1 ]; then
        args_list="ARG"
    elif [ $_arg_count -eq 2 ]; then
        args_list="ARG_1 ARG_2"
    elif [ $_arg_count -gt 2 ]; then
        args_list="ARG_1 ... ARG_$_arg_count"
    fi
    
    echo -n "Usage: "; used=7
    echo -n "$__appname__ "; let used+="1 + ${#__appname__}"
    echo -n "-[$sopt_compact_list] "; let used+="4 + ${#sopt_compact_list}"

    for text in $sopt_list $lopt_list $args_list; do
        text=${text/|/ }
        let nextprint="used + ${#text}" # length if text printed
        if [ $nextprint -gt $LW ]; then
            printf "\n%${TABSIZE}s" "" # print newline and tab spaces
            used=$TABSIZE
        fi
        echo -n "$text "
        let used+="1 + ${#text}"
    done

    echo ""
    
}

# print formatted options table
function print_options_table__ {
    
    # internal constants
    local readonly LW=78 # Line width
    local readonly SP=2  # Spaces at beginning of line
    local readonly SO=4  # Space for short options
    local readonly LO=23 # Max space for long options before it reaches desc
    local SL=0; let SL="SP + SO + LO" # Space taken up by options
    local DE=0; let DE="LW - SL"      # space left for description
    
    # internal vars
    local i=0
    local spaces=0
    local loptarg=""
    local sopt=""
    local lopt=""
    local argn=""
    local desc=""
    local desc_remaining=""
    
    # for each option
    while [ $i -lt $_opt_size ]; do
        
        # read options data
        sopt="-${_opt_short[$i]}"
        lopt="--${_opt_long[$i]}"
        argn="${_opt_args[$i]}"
        desc="${_opt_desc[$i]}"

        # ----- handle short option -----

        # nothing much to do, just print
        printf "%${SP}s${sopt}, "

        # ----- handle long option -----
        
        # Append args to lopt if required
        if [ "$argn" != "0" ]; then lopt="$lopt=$argn"; fi
        
        let spaces="LO - ${#lopt}"
        if [ $spaces -lt 0 ]; then spaces=0; fi

        # print, with spaces appended at back
        printf "%s%${spaces}s" "$lopt" ""
        
        # ----- handle option description -----
        
        # prune desc (remove \n and repeated spaces)
        desc=`echo $desc | tr -d '\n' | tr -s "[:blank:]"`

        # check if desc extends beyond first line, split it up
        desc_remaining=${desc:$DE}
        desc=${desc:0:$DE}
        
        # if long options goes into desc territory, start on new line
        if [ ${#lopt} -gt $LO ]; then
            printf "\n%${SL}s%s\n" "" "$desc"
        else # if it first, go ahead and print description
            echo "$desc"
        fi
        
        # remaining desc text?
        while [ "$desc_remaining" != "" ]; do
            desc=${desc_remaining:0:$DE}
            desc_remaining=${desc_remaining:$DE}
            printf "%${SL}s%s\n" "" "$desc"
        done
        
        let i+=1
    done
}

# debug function ------------------------------------------------------------
function bopt_debug_info {
    
    local i=0
    local optname=""
    local optvname=""
    local optsel=false
    local optval=""

    echo ""
    echo ".. -- (bopt debug data) --"
    echo ".. App Name   : $__appname__"
    echo ".. App Vers   : $__appversion__"
    echo ".. Bug Report : $__bugreport__"
    echo ".. Usage      : $__usage__"
    echo ".. "

    echo ".. Available Options:"
    echo ".. ------------------"
    i=0; while [ $i -lt $_opt_size ]; do
        echo -en ".. ${_opt_long[$i]}\t"
        echo -en "${_opt_short[$i]}\t"
        echo -en "${_opt_args[$i]}\t"
        echo -en "'${_opt_desc[$i]}'\n"
        let i+=1
    done
    echo ".. "
    

    echo ".. Given arguments:"
    echo ".. ----------------"
    echo ".. Arg count  : $(bopt_get_argcount)"
    echo ".. Arg list   : $(bopt_get_arglist)"
    echo ".. "

    echo ".. Given options:"
    echo ".. --------------"
    i=0; while [ $i -lt $_opt_size ]; do
        optname=${_opt_long[$i]}
        optvname=${_opt_args[$i]}
        eval "optsel=\$o_$optname"
        eval "optval=\$v_$optname"
        
        echo -ne ".. $optname\t$optsel"
        if [ "$optvname" != "0" ] && $optsel ; then
            echo -e "\t$optvname=$optval"
        fi
        echo ""
        let i+=1
    done
    
}


# ============= SCRIPT ENDS =================================================

